/*
封装视图的渲染逻辑watcher
*
*/
/**
 * watcher 进行实际的视图渲染
 * 每个组件都有自己的watcher,就可以减少每次更新页面的部分
 * 给每个属性都增加一个dep,目的就是收集watcher
 * 一个视图（组件）可能有很多属性，多个属性对应一个视图，n个dep对应一个watcher
 * 一个属性也可能对应多个视图（组件）
 *
 *
 * 所以 dep和watcher是多对多的关系
 *
 * 每个属性都有自己的dep,属性就是被观察者
 * watcher就说观察者（属性变化了，会通知观察者进行视图更新）-》观察者模式
 *
 *
 * dep:  data中的有多少个属性，就有多少个dep，:dep和data中的属性是一一对应的
 * watcher: 在视图上 用了几个，就有几个watcher, 组件的渲染函数和计算属性各自对应一个 Watcher。
 * dep与watcher关系： 一对多 dep.name = [watcher1,watcher2]   多对多
 *
 * // 二 实现对象的收集依赖
 *
 */

import { pushWatcher, popTargetWatcher } from "./dep"; //通过观察者模式 进行收集依赖
import { nextTick } from "../utils/nextTick";

let id = 0; //来标识每个watcher 区别不同的watcher
export default class Watcher {
  // 目前只有一个watcher实例 因为我只有一个实例 根组件

  /**
   *
   * @param {*} vm 组件实例
   * @param {Function|string} exprOrFn 渲染页面的回调函数 或者函数 或者字符串(需要把字符串转为函数) name:()=>{}, -> ()=>name,()=>{}
   * @param {boolean|object} options 额外选项 true表示初次渲染 对象是额外的配置
   * @param {Function} callback watch等的回调函数
   */
  constructor(vm, exprOrFn, options, callback) {
    console.log(
      vm,
      exprOrFn,
      options,
      callback,
      "Watcher-constructor-初始化方法",
    );
    this.vm = vm;
    this.options = options;
    this.callback = callback;

    // 2. 每一组件只有一个watcher。这个id是唯一标识
    this.id = id++;
    this.user = !!options.user; //取反反 user如果为真那么是watch监听
    this.lazy = options.lazy; // lazy 为true标识是 ---计算属性--默认不渲染视图
    this.dirty = this.lazy; //计算属性---如果dirty为true 表示用户是否执行

    this.deps = []; //存dep
    this.depsId = new Set(); //去重存放 dep id

    if (typeof exprOrFn == "string" && exprOrFn) {
      //这里可能是watch监听 watch: {a:(newval,oldVal) {}}  对象下的表达式exprOrFn 属性'a'  或者 'a.b.c' 是字符串
      // 字符串变成 函数
      this.getter = function () {
        let path = exprOrFn.split("."); //['a','b','c']
        let obj = vm;
        for (let i = 0; i < path.length; i++) {
          // 向obj 实例上去添加字符 然后将其变成函数

          obj = obj[path[i]]; //  vm实例上有a,b,c属性
        }
        console.log(obj, "watch--obj-实例vm上添加该字符");
        return obj;
      };

      this.oldValue = this.get(); // 保存watch监听的初始化值
      console.log(this, "watch--watch监听原理-watch初始化的值");
    } else {
      // 函数
      // vue内容初始化 渲染函数
      this.getter = exprOrFn;
      // 默认调用一次更新视图方法
      // 这里exprOfFn都是函数--有可能是-页面渲染视图-的函数--需要更新页面视图
      //          const updateComponent = () => {
      //     vm._update(vm._render())
      //
      // 也可能是 计算属性的 方法
      //  fullName() {
      //     console.log('计算属性-fullName执行---computed')
      //     return this.firstName + this.lastName
      //   }
      //所以 要区分一下 计算属性函数   和页面渲染 函数

      this.lazy ? "" : this.get();
    }
  }
  addDep(dep) {
    //1. 去重dep
    let id = dep.depId;
    if (!this.depsId.has(id)) {
      this.deps.push(dep);
      this.depsId.add(id);
      //双击记忆 在dep中添加当前watcher
      dep.addSubWatcher(this); // 此处的this===> 当前watcher
    }
  }
  //初次视图渲染--并且把当前watcher存起来，一个关系，，进行依赖收集，看看有哪些地方用到 data中的属性
  get() {
    let watchOldValue;

    pushWatcher(this); //调用添加watcher的方法到 记录当前操作的watcher实例
    watchOldValue = this.getter.call(this.vm); // 页码渲染逻辑 vm._update(vm._render()) ===在调用vm._render方法的时候，便会触发Object.defineProperty中对对象的下的属性的劫持，这个去给当前这个属性下的dep收集当前的watcher实例
    popTargetWatcher(); // 页码渲染完成后，调用，取消当前操作的watcher
    return watchOldValue;
  }

  // 更新方法
  update() {
    // 是计算属性---更新数据的时候-计算属性不走渲染watcher
    if (this.lazy) {
      console.log("更新--computed--11");
      // 依赖的值变化 就标识计算属性的值是脏值了,需要重新计算，最新的计算属性的值，而不是从缓存中拿计算属性的值
      this.dirty = true;
    } else {
      console.log(
        "更新-- computed--22 //  (1) vm._update方法是将vnode变成真实dom   （2）vm._render()方法作用是将render函数转成vnode虚拟dom，返回虚拟dom,也就自动调用vm._update(vm._render())---来更新视图",
      );
      // 页面渲染---函数执行
      // 同步更新视图 改为异步更新视图
      //1. 注意： 不要数据更新后每次都调用get方法，get方法的作用是 会当数据更新后重新更新视图
      //1.2 解决方法是 缓存
      // this.get(); //重新渲染
      queueWatcher(this); //this==>当前的watcher,防抖 比如三次重复触发的函数，只在最后一次调用的时候，生效。
    }
  }

  // watch--实现原理
  run() {
    let newVal;
    newVal = this.get(); // 这里的 get 是个函数 ==》可能是  渲染函数/  watch值函数
    let oldValue = this.oldValue;
    // watch---然后更新this.oldValue 为当前值，下次在更新的时候，当前的值变成老值
    this.oldValue = newVal;

    // 执行 handler == (cb) 这个用户的watch--监听--给个标识来区分 是watch监听的，还是
    if (this.user) {
      this.callback.call(this.vm, newVal, oldValue);
    }
  }

  //计算属性实现原理--- 计算属性来调用的 get方法来和 页面视图进行区分
  evalte() {
    this.value = this.get();
    console.log(this.value, this.getter, "--computed--evalte");
    //表示调用了 一次当前计算属性key对应的 方法了，那么给个标识，后续我就不在调用这个方法了，来实现缓存机制
    this.dirty = false;
  }

  //相互收集
  depend() {
    // 此时计算属性 watcher 先有 然后看看 界面上有没有 使用计算属性，最后肯定是有个渲染watcher来渲染 文本和各种data中的变量的值，
    // 此时的deps中 肯定都是 计算属性 所依赖的 在data中的属性 （dep）
    // 然后前面 判断存在渲染watcher，页面上有用到计算属性，那么当前计算属性 所依赖的 data中的属性（dep）去关联  渲染watcher来实现，，计算属性渲染到页面上的功能
    let i = this.deps.length;
    console.log(
      this.deps,
      "this.deps，--computed-00-depend-因为计算属性所依赖的属性更新的时候，并没有一股脑直接走渲染watcher-更新视图-而且判断是否在 视图中用到计算属性 也就是计算属性watcher运行完之后，还有渲染watcher，来实现动态判断是否需要 调用渲染watcher来更新计算属性的所在页面的视图",
    );
    while (i--) {
      this.deps[i].subScribe();
    }
  }
}

// 用数组模拟队列数据结构 先进先出
let queue = []; // 将需要批量更新的watcher 存放到一个队列中，然后根据id进行去重相同 watcher
let hasWatcherId = new Set();
let pending = false; //防抖状态

function flushWatcher() {
  // 列队真实触发的方法---更新数据-更新dom的地方--我们执行watch实例对象中的callback回调函数，表示数据和dom已经更新完毕，做点其他事情,如： 发布订阅实现，实现在vue组件内的订阅函数 如生命周期函数update
  queue.forEach((item) => {
    item.run(); // watch监听/或者是渲染函数 --值函数---实现原理
    item.callback(); //调用 用户写的生命周期函数 updated等
  });
  console.log("防抖多次触发 只在最后一次触发了");
  // 当前需要执行的方法执行完，重置到初始化，重新记录其他的
  hasWatcherId = new Set();
  queue = [];
  pending = false; //重置到开始状态
}
//通过队列进行优化--进行缓存---不能每次数据更新--每次都调用get方法
function queueWatcher(watcher) {
  console.log(
    watcher.id,
    "queue-watcher，如果我push了三次那么会打印三次相同的watcher ==》id,所以目的就是去重之后，只在最后一次调用get方法进行更新视图",
  );
  let id = watcher.id; //
  if (!hasWatcherId.has(id)) {
    //1.去重
    hasWatcherId.add(id);
    queue.push(watcher);
    //2.防抖 当用户触发多次 只触发最后一次------
    if (pending == false) {
      //2.1 方法1： setTimeout延时处理
      // setTimeout(() => {
      //     queue.forEach(item => item.run())
      //     console.log('防抖多次触发 只在最后一次触发了')
      //     hasWatcherId = new Set()
      //     queue = []
      //     pending = false; //重置到开始状态

      // }, 0);

      //2.2 方法2 实现nextTick方法的内部功能=-==》异步处理---也就是nextTick()的原理===》相当于定时器
      nextTick(flushWatcher);
    }
    //3. 防抖在延时器没触发之前 只满足第一次if进入延时器的条件 --下面这个是同步代码 优先执行在异步宏任务之前
    pending = true;
  }
  console.log(
    queue,
    hasWatcherId,
    "调用了触发了很多次的queuequeue---hasWatcherIdhasWatcherId",
  );
}
